/**
 * @author Created by felix on 18-7-2.
 * @email   307253927@qq.com
 */
'use strict';

class Resampler {
  constructor(fromSampleRate, toSampleRate, channels, inputBufferSize) {
    
    if (!fromSampleRate || !toSampleRate || !channels) {
      throw(new Error("Invalid settings specified for the resampler."));
    }
    this.resampler       = null;
    this.fromSampleRate  = fromSampleRate;
    this.toSampleRate    = toSampleRate;
    this.channels        = channels || 0;
    this.inputBufferSize = inputBufferSize;
    this.initialize()
  }
  
  initialize() {
    if (this.fromSampleRate == this.toSampleRate) {
      
      // Setup resampler bypass - Resampler just returns what was passed through
      this.resampler   = (buffer) => {
        return buffer
      };
      this.ratioWeight = 1;
      
    } else {
      
      if (this.fromSampleRate < this.toSampleRate) {
        
        // Use generic linear interpolation if upsampling,
        // as linear interpolation produces a gradient that we want
        // and works fine with two input sample points per output in this case.
        this.linearInterpolation();
        this.lastWeight = 1;
        
      } else {
        
        // Custom resampler I wrote that doesn't skip samples
        // like standard linear interpolation in high downsampling.
        // This is more accurate than linear interpolation on downsampling.
        this.multiTap();
        this.tailExists = false;
        this.lastWeight = 0;
      }
      
      // Initialize the internal buffer:
      this.initializeBuffers();
      this.ratioWeight = this.fromSampleRate / this.toSampleRate;
    }
  }
  
  bufferSlice(sliceAmount) {
    
    //Typed array and normal array buffer section referencing:
    try {
      return this.outputBuffer.subarray(0, sliceAmount);
    }
    catch (error) {
      try {
        //Regular array pass:
        this.outputBuffer.length = sliceAmount;
        return this.outputBuffer;
      }
      catch (error) {
        //Nightly Firefox 4 used to have the subarray function named as slice:
        return this.outputBuffer.slice(0, sliceAmount);
      }
    }
  }
  
  initializeBuffers() {
    this.outputBufferSize = (Math.ceil(this.inputBufferSize * this.toSampleRate / this.fromSampleRate / this.channels * 1.000000476837158203125) + this.channels) + this.channels;
    try {
      this.outputBuffer = new Float32Array(this.outputBufferSize);
      this.lastOutput   = new Float32Array(this.channels);
    }
    catch (error) {
      this.outputBuffer = [];
      this.lastOutput   = [];
    }
  }
  
  linearInterpolation() {
    this.resampler = (buffer) => {
      let bufferLength = buffer.length,
          channels     = this.channels,
          outLength,
          ratioWeight,
          weight,
          firstWeight,
          secondWeight,
          sourceOffset,
          outputOffset,
          outputBuffer,
          channel;
      
      if ((bufferLength % channels) !== 0) {
        throw(new Error("Buffer was of incorrect sample length."));
      }
      if (bufferLength <= 0) {
        return [];
      }
      
      outLength    = this.outputBufferSize;
      ratioWeight  = this.ratioWeight;
      weight       = this.lastWeight;
      firstWeight  = 0;
      secondWeight = 0;
      sourceOffset = 0;
      outputOffset = 0;
      outputBuffer = this.outputBuffer;
      
      for (; weight < 1; weight += ratioWeight) {
        secondWeight    = weight % 1;
        firstWeight     = 1 - secondWeight;
        this.lastWeight = weight % 1;
        for (channel = 0; channel < this.channels; ++channel) {
          outputBuffer[outputOffset++] = (this.lastOutput[channel] * firstWeight) + (buffer[channel] * secondWeight);
        }
      }
      weight -= 1;
      for (bufferLength -= channels, sourceOffset = Math.floor(weight) * channels; outputOffset < outLength && sourceOffset < bufferLength;) {
        secondWeight = weight % 1;
        firstWeight  = 1 - secondWeight;
        for (channel = 0; channel < this.channels; ++channel) {
          outputBuffer[outputOffset++] = (buffer[sourceOffset + ((channel > 0) ? (channel) : 0)] * firstWeight) + (buffer[sourceOffset+(channels + channel)] * secondWeight);
        }
        weight += ratioWeight;
        sourceOffset = Math.floor(weight) * channels;
      }
      for (channel = 0; channel < channels; ++channel) {
        this.lastOutput[channel] = buffer[sourceOffset++];
      }
      return this.bufferSlice(outputOffset);
    };
  }
  
  multiTap() {
    this.resampler = (buffer) => {
      let bufferLength = buffer.length,
          outLength,
          output_variable_list,
          channels     = this.channels,
          ratioWeight,
          weight,
          channel,
          actualPosition,
          amountToNext,
          alreadyProcessedTail,
          outputBuffer,
          outputOffset,
          currentPosition;
      
      if ((bufferLength % channels) !== 0) {
        throw(new Error("Buffer was of incorrect sample length."));
      }
      if (bufferLength <= 0) {
        return [];
      }
      
      outLength            = this.outputBufferSize;
      output_variable_list = [];
      ratioWeight          = this.ratioWeight;
      weight               = 0;
      actualPosition       = 0;
      amountToNext         = 0;
      alreadyProcessedTail = !this.tailExists;
      this.tailExists      = false;
      outputBuffer         = this.outputBuffer;
      outputOffset         = 0;
      currentPosition      = 0;
      
      for (channel = 0; channel < channels; ++channel) {
        output_variable_list[channel] = 0;
      }
      
      do {
        if (alreadyProcessedTail) {
          weight = ratioWeight;
          for (channel = 0; channel < channels; ++channel) {
            output_variable_list[channel] = 0;
          }
        } else {
          weight = this.lastWeight;
          for (channel = 0; channel < channels; ++channel) {
            output_variable_list[channel] = this.lastOutput[channel];
          }
          alreadyProcessedTail = true;
        }
        while (weight > 0 && actualPosition < bufferLength) {
          amountToNext = 1 + actualPosition - currentPosition;
          if (weight >= amountToNext) {
            for (channel = 0; channel < channels; ++channel) {
              output_variable_list[channel] += buffer[actualPosition++] * amountToNext;
            }
            currentPosition = actualPosition;
            weight -= amountToNext;
          } else {
            for (channel = 0; channel < channels; ++channel) {
              output_variable_list[channel] += buffer[actualPosition + ((channel > 0) ? channel : 0)] * weight;
            }
            currentPosition += weight;
            weight = 0;
            break;
          }
        }
        
        if (weight === 0) {
          for (channel = 0; channel < channels; ++channel) {
            outputBuffer[outputOffset++] = output_variable_list[channel] / ratioWeight;
          }
        } else {
          this.lastWeight = weight;
          for (channel = 0; channel < channels; ++channel) {
            this.lastOutput[channel] = output_variable_list[channel];
          }
          this.tailExists = true;
          break;
        }
      } while (actualPosition < bufferLength && outputOffset < outLength);
      return this.bufferSlice(outputOffset);
    };
  }
  
  resample(buffer) {
    if (this.fromSampleRate == this.toSampleRate) {
      this.ratioWeight = 1;
    } else {
      if (this.fromSampleRate < this.toSampleRate) {
        this.lastWeight = 1;
      } else {
        this.tailExists = false;
        this.lastWeight = 0;
      }
      this.initializeBuffers();
      this.ratioWeight = this.fromSampleRate / this.toSampleRate;
    }
    return this.resampler(buffer)
  }
}

module.exports = Resampler